HOME/Articles/

React Hooks的基本介绍

Article Outline

上篇介绍了React的基本使用,这篇博客介绍一下著名的React Hooks

<!--more-->

Hooks简介

注意:React 16.8.0 是第一个支持 Hook 的版本

Hooks一些可以让你在函数组件里“钩入”React state及生命周期等特性的函数。其提供了使函数式组件可以使用和Class组件一样的特性的方法,例如useState可以让函数式组件也拥有state等。

Hooks带来的好处有:

  • 可以使用Hook从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • Hook组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分
  • Hook使你在非class的情况下可以使用更多的React特性

Hooks的使用原则

Hook就是JavaScript函数,但是使用它们会有两个额外的规则:

  1. 只能在函数最外层调用Hook。不要在循环、条件判断或者子函数中调用。
  2. 只能在React函数组件中调用Hook。不要在其他JavaScript函数中调用。(当然自定义的Hook中也可以调用)

State Hook

State Hook的简单使用

先来看一个最简单的使用useState()的例子:

import React, {useState} from "react";

export default () => {
    const [counter,  setCounter] = useState(0); // 0代表给counter的初始值为0

    let btnClick = () => (
        setCounter(counter + 1)
    );

    return (
        <div>
            <h1>LearnHooks</h1>
            <div>current Counter: {counter}</div>
            <button onClick={btnClick}>click to plus counter</button>
        </div>
    )
}

上述组件如果使用Class组件的写法,等价于:

export default class LearnReact extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
        }
    }

    btnClick = () => {
        this.setState((prevState) => {
            return {
                counter: prevState.counter + 1
            }
        })
    };

    render() {
        return (
            <div>
                <h1>LearnHooks</h1>
                <div>current Counter: {this.state.counter}</div>
                <button onClick={this.btnClick}>click to plus counter</button>
            </div>
        )
    }
}

仔细体会一下两种写法的差异和优劣性,下面来仔细分析一下state hook的使用:

  1. const [counter, setCounter] = useState(0)

    这句话定义了一个counter变量和一个用来修改定义的counter变量的方法setCounter(),定义的这个变量它与 class组件里面的this.state提供的功能完全相同。一般来说,在函数退出后变量就就会”消失”,state中的变量会被React保留

  2. useState(0)

    useState()只接收一个参数即定义的state变量的初始值,可以是对象也可以是其他原始类型等等。而Class组件的初始值则一定是在this.state中的这个对象里的属性,这是一个区别点。

  3. useState()的返回值

    经过上面的例子,我们已经可以得出,useState的返回值是一个数组,其内部元素依次为当前state以及更新state的函数, 每定义一个state都需要去成对的获取一下修改相应的state的方法。

useState(initialValue)是一个惰性的初始值,一旦初始化之后,后续initialValue就算有更新也会被忽略

state Hook中的事件处理函数

对于上述例子,更新state时传入的事件处理函数,注意不能直接写成:

<button onClick={setCounter(counter + 1)}>click to plus counter</button> // error!

而是得写成回调形式,否则会直接执行一次setCounter(counter + 1)造成无限循环render:

<button onClick={() => setCounter(counter + 1)}>click to plus counter</button> // correct!

定义多个state

当想要定义多个state时,重复调用多次useState就行了:

const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

但是在开发时要注意state的分离颗粒度。

state hook的更新

值得注意的是, useState返回的修改state的方法对于state的修改,是单纯的替换而不是合并

useState返回的修改state的方法对于state的修改,是单纯的替换而不是合并,来看一个例子:

import React, {useState} from "react";

export default () => {
    const [obj, setObj] = useState({
        name: "Yang",
        age: 23,
        gender: "male"
    });

    let changeObj = () => (
        setObj({
            name: "Zhang"
        })
    );

    return (
        <div>
            <h1>LearnHooks</h1>
            {
                Object.keys(obj).map(key => {
                    return (
                        <div key={key}>key: {obj[key]}</div>
                    )
                })
            }
            <button onClick={changeObj}>click to changeObj</button>
        </div>
    )
}

上述例子中,调用setObj({name: "Zhang"})之后,

obj的值由{name: "Yang", age: 23, gender: "male"}直接变为了{name: "Zhang"}

并没有像传统的class组件中调用setState那样对值进行合并,这一点要特别注意。

State Hook对于state的更新方法,也像class那样可以传入一个函数进行函数式更新

setCounter((prevCounter) => {
    return prevCounter + 1;
})

Effect Hook

Effect Hook是针对于那些副作用操作(比如:数据获取设置订阅以及手动更改React组件中的DOM等)而使用的。

class组件做比较的话,Effect Hook可以视为componentDidMountcomponentDidUpdatecomponentWillUnmount这三个钩子的组合。

无需清除的effect的简单使用

来看一个简单的例子:

import React, {useState, useEffect} from "react";

export default () => {
    const [counter, setCounter] = useState(0);
    useEffect(() => {
        //    第一次渲染之后和每次更新之后都会执行
        document.title = `current Counter: ${counter}`
    });

    return (
        <div>
            <h1>LearnHooks</h1>
            <div>current Counter: {counter}</div>
            <button onClick={() => setCounter(counter + 1)}>click to plus counter</button>
        </div>
    )
}

仔细的来分析一下这个最简单的例子:

首先我们定义了一个无需清除的useEffect,其内部接收一个函数作为参数,其内部的逻辑在默认情况下,在组件第一次渲染之后和每次更新之后都会执行

然后由于其定义在函数内部,所以当前函数组件的stateprops我们都可以在useEffect内部访问到

useEffect传递的函数作为参数,会被称为effectReact保存起来,在这个函数内部,可以执行任意的副作用操作,React保证了每次运行effect的同时,DOM都已经更新完毕

注意:由于useEffect其是异步的,所以不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快,但是如果需要effect同步执行,请使用useLayoutEffect

上面这样使用useEffect有一个潜在的好处是,开发者没必要去关心当前这个组件到底是第一次渲染还是处于更新状态

使用Class组件时经常会有componentDidMountcomponentDidUpdate中存在相同逻辑的地方,useEffect使得这部分逻辑获得了简化

需要清除的effect

一般使用Class组件时,我们会在componentDidMount进行一些数据的订阅,在componentWillUnmount中取消这部分的订阅

对于这样的effect,我们使用useEffect时就要有不一样的逻辑了

一般使用Class组件,我们需要将订阅和取消订阅操作放到2个不同的钩子函数中,但是使用useEffect时,这样的操作是放到一起的。

只要在useEffectreturn出一个函数后,返回的这个函数就会在执行清除操作时(React会在组件卸载的时候执行清除操作)调用它,这是useEffect的一个可选的清除机制

所以一般需要清除的effect的代码大概像这样:

    useEffect(() => {
        //    第一次渲染之后和每次更新之后都会执行
        Api.subscribeXXX(xxx);

        return () => { //  return的函数会在React执行清除操作时调用
            Api.unsubscribeXXX(xxx);
        }
    });

接下来看一些effect常见的进阶用法

使用多个Effect将不相关的逻辑分离开

使用Class组件的一个不好的地方就是开发者会被迫将不相关的逻辑放到同一个钩子函数中,跟Vueoptions Api以及composition Api是一个道理。

useEffect也像useState一样允许开发者定义多个,可以在同一个useEffect中专注于同一逻辑。例如:

export default () => {
    const [counter, setCounter] = useState(0);
    useEffect(() => { // 这个effect 只处理counter相关逻辑
        document.title = `current Counter: ${counter}`;
    });

    useEffect(() => { // 这个effect只处理订阅逻辑
        Api.subscribeXXX(xxx);

        return () => { //  return的函数会在React执行清除操作时调用
            Api.unsubscribeXXX(xxx);
        }
    });

    return (
        <div>
            <h1>LearnHooks</h1>
            <div>current Counter: {counter}</div>
            <button onClick={() => setCounter(counter + 1)}>click to plus counter</button>
        </div>
    )
}

如上例所示,我们可以根据代码的用途去定义多个effect

由于effect在每次重渲染时都会执行导致的性能问题及解决方案

我们一直在强调,effect组件第一次渲染及之后每次更新都会执行

这样做的好处是解决了Class组件中经常存在的忘记在componentDidUpdate钩子中添加组件更新后的逻辑的问题

但是这样每次渲染后都执行清理或者执行effect也带来了性能问题

传统的Class组件可以在componentDidUpdate中进行对比prevPropsprevState来进行跳过执行逻辑

相应的,使用useEffect也有对应的功能:

useEffect(() => { // 这个effect 只处理counter相关逻辑
    document.title = `current Counter: ${counter}`;
}, [counter]); // 仅在 counter 更改时更新

我们可以通过给useEffect传递一个数组作为其第二个参数来达到效果,如果某些特定值在两次重渲染之间没有发生变化,就可以跳过对effect的调用

值得注意的是,如果数组中有多个元素,即使只有一个元素发生变化,React也会执行effect

如果想执行只运行一次的effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉React你的effect不依赖于propsstate中的任何值,所以它永远都不需要重复执行。

如果你传入了一个空数组([]),effect内部的propsstate就会一直会是其初始值。

另外React会等待浏览器完成画面渲染之后才会延迟调用useEffect,因此会使得额外操作很方便。

自定义Hook

自定义Hook是一个函数,其名称必须以use开头,函数内部可以调用其他的Hook

每次使用自定义Hook时,其中的所有state和副作用都是完全隔离独立的

来看一个使用自定义Hook的例子

假设现在有一个记录页面已经打开了多少秒的组件如下:

export default () => {
    const [seconds, setSeconds] = useState(0);
    let timer;

    useEffect(() => {
        timer = setInterval(() => {
            setSeconds(seconds + 1);
        }, 1000);

        return () => {
            console.log("Total Seconds: ", seconds);
            clearInterval(timer);
        }
    });

    return (
        <div>
            <span>页面已经渲染了{seconds}秒</span>
        </div>
    )
}

这时别的组件刚好也需要这个计时功能,就可以将其内部计数的逻辑单独抽出来,定义为一个自定Hook,比如我们定义为useSeconds,内部逻辑为:

// useSeconds.js
import {useState, useEffect} from "react";

export default function useSeconds() {
    const [seconds, setSeconds] = useState(0);
    let timer;

    useEffect(() => {
       timer = setInterval(() => {
           setSeconds(seconds + 1);
       },1000);
       return () => {
           console.log("Total Seconds: ", seconds);
           clearInterval(timer);
       }
    });

    return seconds;
}

此时我们就可以进行使用这个自定义Hook了,在原来的组件里:

import React from "react";
import useSeconds from "./useSeconds";

export default () => {
    const seconds = useSeconds();

    return (
        <div>
            <span>页面已经渲染了{seconds}秒</span>
        </div>
    )
}

在另外想复用的组件里也可以直接引入使用,且多个自定义Hook之间的stateeffect是相互独立的。

当然由于自定义Hook就是一个函数,也可以通过调用使用传入参数传递信息。

从上例中我们可以看出:自定义Hook解决了以前在React组件中无法灵活共享逻辑的问题。

useContext

接收一个context对象(React.createContext的返回值)并返回该context的当前值。该Hook能够读取context的值以及订阅context的变化。

来看一个简单使用的例子, 假设有如下Theme文件:

import React from "react";
const themes = {
    light: {
        foreground: "#000000",
        background: "#eeeeee"
    },
    dark: {
        foreground: "#ffffff",
        background: "#222222"
    }
};
const ThemeContext = React.createContext(themes.light);
export  {
    themes,
    ThemeContext,
}

此时在<App />中应用这个Context:

import React, {useState, useEffect} from 'react';
import LearnHooks from "./components/LearnHooks";
import { themes, ThemeContext } from "./components/Theme";

function App() {
    const [theme, setTheme] = useState(themes.dark);

    useEffect(() => {
        setTimeout(() => {
            setTheme(themes.light); // 3s后将主题改为白色
        }, 3000)
    });

    return (
        <div id="app">
            <ThemeContext.Provider value={theme}>
                <LearnHooks/>
            </ThemeContext.Provider>
        </div>
    );
}

export default App;

此时在我们的目标组件中,就可以进行使用useContext来进行获取Context了:

import React, {useContext} from "react";
import { ThemeContext } from "./Theme";

export default () => {
    const theme = useContext(ThemeContext); // 获取theme

    return (
        <p style={{ background: theme.background, color: theme.foreground }}>
            normal Text
        </p>
    )
}

useReducer

用法:

const [state, dispatch] = useReducer(reducer, initialArg, init);

state逻辑较复杂且包含多个子值,或者下一个state依赖于之前的state等场景下,可以用来代替useState(),同时useReducer的优势在于还会对深层次组件更新做优化。

useReducer最多可以接收三个参数:

  • 第一个参数reducer(state, action) => newState类型的函数
  • 第二个参数如果在第三个参数未传的情况下,是直接作为state的初始值的,但是如果传入了第三个参数,那么初始值为init(initialArg)
  • 第三个参数是可选的一个函数,参数为initialArg, 返回state的初始值(传入init时为惰性的初始化state)。

看一个基本使用的例子:

import React, {useReducer} from "react";

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
}

function init(initialCount) {
    return {count: initialCount};
}

export default () => {
    const [state, dispatch] = useReducer(reducer, 0, init);

    return (
        <div>
            Count: {state.count}
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </div>
    )
}

useCallbackuseMemo

这2个hook都是作为性能优化手段来使用的,也能使用其特性达成一些特殊用途。且useMemo可以实现useCallback

相关用法:

// useCallback:
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

// useMemo:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useCallback(fn, deps)相当于useMemo(() => fn, deps)

2者都是返回一个memoized过后的函数/值,第二个参数和useEffect类似为依赖项,如果依赖项有改变的话,memoized的值或者函数才会得到更新。

如果依赖传入一个[]或者依赖未发生改变的话,其memoized的函数或者值的引用地址是不会改变的(同一块内存区域),利用这一特性,可以配合类似于shouldComponentUpdate的机制来进行避免重复渲染。

使用useCallbackuseMemo的场景举例

了解了基本概念之后,这篇文章举了个例子展示了useCallbackuseMemo的使用。

大概例子是有这么一个防抖函数,在鼠标滑动的时候去触发:

// generateDebounce.js

function generateDebounce(func, delay=1000) {
    let timer;
    function debounce(...args) {
        debounce.cancel();
        timer = setTimeout(() => {
            console.count("func called");
            func.apply(this, args);
        }, delay);
    }

    debounce.cancel = function () {
        if (timer !== undefined) {
            clearTimeout(timer);
            timer = undefined;
        }
    };
    return debounce;
}

这个函数调用之后返回一个防抖函数debounce,然后在如下组件中进行防抖使用:

import React, {useState} from "react";
import generateDebounce from "./generateDebounce";

export default () => {
    const [count, setCount] = useState(0);
    const [bounceCount, setBounceCount] = useState(0);
    const debounceSetCount = generateDebounce(setBounceCount); // 每次更新渲染都会重新创建一个debounceSetCount

    const handleMouseMove = () => {
        setCount(count + 1);
        debounceSetCount(bounceCount + 1);
    };

    return (
        <div onMouseMove={handleMouseMove}>
            <p>普通移动次数: {count}</p>
            <p>防抖处理后移动次数: {bounceCount}</p>
        </div>
    )
}

在上述例子中,我们可以看到,虽然bounceCount增加的不多,但是其实内部的console.count("func called");执行的次数和未做防抖的次数count是一样的

也就是说并没有达到防抖的效果,造成这个现象的原因是:

每次执行onMouseMove都会导致组件的重新渲染,整个函数组件将会被重新执行

即意味着const debounceSetCount = generateDebounce(setBounceCount);这句每次都会执行,会创建很多个新的debounceSetCount,所以其不同的debounce其实是使用很多个不同的timer,这就造成了我们看到的调用次数并没有减少的情况

但是bounceCount增加的并没有像count那么快的原因就是在执行onMouseMove时疯狂的传入了很多次一样的参数,而在异步函数中执行增加操作时,其实都是一个相同的值在加一,所以bounceCount没有增加到函数调用次数那么大,但是本质上,函数还是调用了很多次的。

使用useCallback举例

花了这么多篇幅讲通这个例子的原路,现在来看怎么修复,我们通过useCallback创建一个memoized函数,依赖为[], 这样一来,我们创建的这个debounceSetCount函数的引用就一直是同一个地址,这样就组件每次更新时,由于依赖为[],函数一直不会更新,永远为同一个函数,即可达到效果

export default () => {
    const [count, setCount] = useState(0);
    const [bounceCount, setBounceCount] = useState(0);
    // const debounceSetCount = generateDebounce(setBounceCount);
    // 改用callback创建一个 memoized 函数,依赖为[]即永远保存同一块内存中的这个 debounceSetCount 函数
    const debounceSetCount = useCallback(generateDebounce(setBounceCount), []);

    // 省略下面代码。。。。
}

使用useMemo举例

上面例子中,也可以直接使用useMemo:

    // const debounceSetCount = generateDebounce(setBounceCount);
    // const debounceSetCount = useCallback(generateDebounce(setBounceCount), []);
    const debounceSetCount = useMemo(() => generateDebounce(setBounceCount), []);

达到的效果是一样的。也能创建一个唯一的debounceSetCount函数

关于useMemo,官方建议我们,先不要使用useMemo编写可用的代码,然后再引入useMemo仅仅作为性能优化的手段,因为官方说了useMemo不一定能作为一个保证来使用

关于useMemo引自文档: You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components.

useRef

用法:

const ref = useRef(initialValue);

Class组件一样,useRef提供了在函数组件中使用ref的方法,其参数initialValue为给ref设置的初始值,该值在useEffect之中就已经被重新赋值为目标DOM

来看使用例子:

import React, {useRef, useEffect} from "react";


export default () => {
    const testRef = useRef(null); // 给null作为初始值
    console.log(testRef); // {current: null}

    useEffect(() => {
        console.log(testRef); // 输出 {current: div}
    });

    return (
        <div ref={testRef}>
            normal Text
        </div>
    )
}

ref对象内容发生变化时,useRef并不会通知更新。且变更.current属性也不会引发组件重新渲染。

如果想要在React绑定或解绑DOM节点的ref 时运行某些代码,则需要使用回调ref来实现。

来看一个不使用useRef而是使用回调ref的例子:

export default () => {
    const [isShow, setIsShow] = useState(true);
    const callbackRef = useCallback((domNode) => {
        console.log(domNode); // 在ref附加到节点上时自动调用  在节点卸载时也会自动调用 输出null
    }, []);

    return (
        <React.Fragment>
            {
                isShow &&
                <h1 ref={callbackRef}>
                    <div>Hello, ref</div>
                </h1>
            }
            <button onClick={() => (setIsShow(false))}>click</button>
        </React.Fragment>
    )
}

我们分析下上述例子:使用useCallback声明一个callbackRef,传入的依赖为[],所以其ref不会在组件重新渲染时改变。

使用回调ref的优点是,节点发生变化的时候,会自动调用目标回调,而使用useRef时,节点对象发生变化时,useRef并不会通知你(当然可以手动写一个useEffect去主动获取ref对象,是可以拿到最新的对象的)

useImperativeHandle

用法:

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle是和forwardRef搭配使用实现refs转发的,来看使用例子,现有父组件:

export default () => {
    const supRef = useRef(null);

    useEffect(() => {
        console.log(supRef);
        supRef.current.focus();
    });

    return (
        <ImperativeHandle ref={supRef} />
    )
}

而子组件里的逻辑为:

// ImperativeHandle.js
import React, {useRef, useImperativeHandle} from "react";

export default React.forwardRef((props, ref) => {
    const subRef = useRef(null);

    useImperativeHandle(ref, () => subRef.current);

    return <input ref={subRef} />;
})

useImperativeHandle的功能在于,在使用ref时自定义暴露给父组件的实例值,上述例子中我们通过使用:

useImperativeHandle(ref, () => subRef.current);

直接暴露出了整个subRef.current,我们可以自定义决定暴露出什么,比如我们改为暴露一个subFocus方法而不是暴露整个subRef:

// ImperativeHandle.js
    <!--省略其他代码-->
    useImperativeHandle(ref, () => { // 可以自定义决定暴露什么内容给父组件
        return {
            subFocus: () => {
                subRef.current.focus();
            }
        }
    });

在父组件中获取到的supRef.current也发生了相应的改变:

export default () => {
    const supRef = useRef(null);

    useEffect(() => {
        // 在这获取到的supRef.current 就是子组件通过 useImperativeHandle 自定义暴露出的内容
        supRef.current.subFocus(); // 调用暴露出的subFocus()
    });

    return (
        <ImperativeHandle ref={supRef} />
    )
}

useLayoutEffect

useLayoutEffectuseEffect的区别在于:useLayoutEffect会在所有的DOM变更之后同步调用effect

可以使用它来读取DOM布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。

其调用阶段和componentDidMountcomponentDidUpdate 的调用阶段是一样的

但是一般只有特殊情况才会使用到,一般建议使用useEffect来避免阻塞加载从而提高用户体验

一些补充

react hooks 异步获取数据

扩展阅读:How to fetch data with React Hooks?

useRef的额外用法

useRef不仅可以用于DOM refsref对象还是一个current属性可变且可以容纳任意值的通用容器,可以作为当前组件中的全局变量进行使用。

比如说:作为一个timerID,在卸载组件的时候进行消除

使用useRefeffect只在组件更新时执行

通过使用useRef,可以达到一个使useEffect只在组件更新时(类似于componentDidUpdate)进行执行effect而在组件第一次渲染时(类似于componentDidMount)不执行effect:

 export default () => {
    const [counter, setCounter] = useState(0);
    const isFirstRender = useRef(true); // 设置默认 是否第一次渲染为true

    useEffect(() => {
        if(!isFirstRender.current) { // 已经不是第一次渲染 而是后续组件更新
            console.log("componentDidUpdate");
            //    目标 effect的逻辑可以在这执行
        }else {
            isFirstRender.current = false; // 第一次渲染之后将值置为false
            console.log("componentDidMount");
        }
    });

    return (
        <div>
            <div>counter: {counter}</div>
            <button onClick={() => setCounter(counter + 1)}>click to reRender component</button>
        </div>
    )
}

通过useRef获取上一轮的props或者state

可以通过useRefuseEffect来进行记录存储上一次的state:

export default () => {
    const [counter, setCounter] = useState(0);
    const prevCounter = useRef();

    useEffect(() => {
        prevCounter.current = counter;
        console.log("counter: ", counter);
        console.log("prevCounter: ", prevCounter.current); // 这里获取的prevCounter和counter是一致的
        //  这的逻辑是较晚异步执行的
    });

    console.log(prevCounter.current); // 在这里获取的prevCounter 为前一次的值
    // useEffect是异步执行  所以在这的逻辑是较早执行的
    return (
        <div>
            <div>counter: {counter}</div>
            <div>prevCounter: {prevCounter.current}</div>
            <button onClick={() => setCounter(counter + 1)}>click to reRender component</button>
        </div>
    )
}

如果该逻辑经常用到的话,可以考虑封装为一个自定义hook:

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

组件中的函数读取stateprop出现不及时更新的情况

造成在组件内函数中拿到的stateprop不是最新的原因有两个:

  1. 如果是使用的useEffect,可能是依赖数组中提供了[]或者依赖项没有提供全.
  2. 组件内部的任何函数,包括事件处理函数和effect,其内部拿到的值都是其被声明的那一次渲染中获取的

其中第1点可能很好理解,解决方案就是修正给useEffect提供的依赖数组即可

下面解释一下第2点:假设现有如下例子:

export default () => {
    const [counter, setCounter] = useState(0);

    function handleAlertClick() {
        setTimeout(() => {
            alert('You clicked on: ' + counter);
        }, 3000);
    }

    return (
        <div>
            <p>You clicked {counter} times</p>
            <button onClick={() => setCounter(counter + 1)}>
                Click me
            </button>
            <button onClick={handleAlertClick}>
                Show alert
            </button>
        </div>
    )
}

在上述组件中,点击了show alert按钮之后,再点击数次click me按钮去增加counter 可以看到,3s后会输出当时3s前点击show alert时的counter,而不是目前页面显示的counter

要理解这种情况发生的原因,需要理解2点:

  1. 每次点击click me去更新state的时候,整个函数组件的逻辑都会被重新执行,所以事件处理函数handleAlertClick每次都会被重复声明
  2. 明确了第1点之后,那么每次重新声明的handleAlertClick内部都只能拿到当前这次渲染中的state

我们在上例中,先点击一次handleAlertClick,其只能拿到当前这次渲染时的counter0,然后我们点击数次click me并不会改变第一次声明的这个handleAlertClick中拿到的counter值,所以即会造成上述情况。

而这种情况,也是会造成在函数中拿到的值是陈旧的情况,针对这种情况,如果想要去获取到最新的stateprop的话,可以在值更新后的异步回调中去创建一个ref去存储其最新值。

使用ref存储的例子:

export default () => {
    const [counter, setCounter] = useState(0);
    const latestVal = useRef(null);

    function handleAlertClick() {
        setTimeout(() => {
            alert('You clicked on: ' + counter);
            alert('latest counter: ' + latestVal.current);
        }, 3000);
    }

    useEffect(() => {
        latestVal.current = counter; // 在这使用ref存储最新的值
    }, [counter]);

    return (
        <div>
            <p>You clicked {counter} times</p>
            <button onClick={() => setCounter(counter + 1)}>
                Click me
            </button>
            <button onClick={handleAlertClick}>
                Show alert
            </button>
        </div>
    )
}

在函数组件中使用hook达到static getDerivedStateFromProps的效果

如果你的组件中state的值在任何时候都取决于props的时候,这种情况才考虑使用static getDerivedStateFromProps, 用之前考虑一下,如果不是这种情况,那么you-probably-dont-need-derived-state

针对这种情况,React有一个机制是:如果在渲染过程中更新state的话,那么React立即退出上一次渲染并用更新后的 state重新运行组件以避免耗费太多性能

function ScrollView({row}) {
    let [isScrollingDown, setIsScrollingDown] = useState(false);
    let [prevRow, setPrevRow] = useState(null);

    // 每次父组件props.row改变时,在这做拦截判断,如果没改变那么按逻辑return,
    // 如果props.row改变了,那么直接在这setState更新,跳过当前这次渲染,直接使用新的state运行下次组件逻辑
    if (row !== prevRow) {
        // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
        setIsScrollingDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
    }

    return `Scrolling down: ${isScrollingDown}`;
}

上述是官方举的一个例子,该组件接收一个propsrow,目标组件中state依赖props.row进行更新,本组件中如果父组件滚动时,子组件的逻辑会判断props.row,如果确定向下滚动了,那么直接调用setState更新state,跳过当前这次渲染,直接使用新的state运行下次组件逻辑

实现类似于React.PureComponent的效果

可以通过React.memo来达到效果:

const Button = React.memo((props) => {
  // 你的组件
});

React.memo等效于PureComponent,但它只比较props, 不比较state

也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新

// 第二个参数指定一个自定义的比较函数来比较新旧 props
const compare = (prevProp, currentProp) => {
    return prevProp.children === currentProp.children; // return true 代表跳过更新
};

export default React.memo((props) => {
    return (
        <div>
            <div>hello world</div>
            {props.children}
        </div>
    )
}, compare)